package cn.open.feishu.alert.common.utils;

import cn.open.feishu.alert.config.HttpClientProperties;
import cn.open.feishu.alert.context.SpringContext;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.AbstractHttpMessage;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.pool.PoolStats;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Description http请求工具类
 * @Author fandy.lin
 * @Date 2021/11/20 4:20 下午
 **/
@Slf4j
public class HttpUtil {
    private static final int LARGE_ELAPSED_TIME = 1000;
    private static final int HTTP_SUCC = 200;
    private static volatile CloseableHttpClient HTTP_CLIENT;

    private static CloseableHttpClient getClient() {
        if (HTTP_CLIENT == null) {
            synchronized (HttpUtil.class) {
                if (HTTP_CLIENT == null) {
                    HTTP_CLIENT = new ApacheHttpClientDelegate(SpringContext.getBean(HttpClientProperties.class)).create();
                }
            }
        }
        return HTTP_CLIENT;
    }

    public static <T> T execute(String url, Map<String, String> header, Object entity, Class<T> clz) throws IOException {
        HttpPost post = new HttpPost(url);
        appendDefaultHeader(post, header);
        if (entity != null) {
            if (entity instanceof List && ((List) entity).size() > 0) {
                if (((List) entity).get(0) instanceof NameValuePair) {
                    post.setEntity(new UrlEncodedFormEntity((List<NameValuePair>) entity, Charset.forName("UTF-8")));
                }
            } else {
                post.setEntity(new StringEntity(JSON.toJSONString(entity), Charset.forName("UTF-8")));
            }
        }
        if (clz.equals(String.class)) {
            return (T) execute(post);
        } else {
            return JSON.parseObject(execute(post), clz);
        }
    }

    private static void appendDefaultHeader(AbstractHttpMessage httpMessage, Map<String, String> header) {
        httpMessage.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
        if (Objects.isNull(header)) {
            return;
        }
        for (Map.Entry<String, String> entry : header.entrySet()) {
            httpMessage.addHeader(entry.getKey(), entry.getValue());
        }
    }

    /**
     * 处理各种请求失败，包括错误异常，以及错误码等
     *
     * @param request
     * @return
     */
    private static String execute(HttpUriRequest request) throws IOException {
        long s = System.currentTimeMillis();
        log.debug("HttpUtil request start :{}", request.getURI());
        CloseableHttpResponse response = null;
        try {
            try {
                response = getClient().execute(request);
            } catch (IOException e) {
                log.warn("发送请求时出现异常", e);
                throw e;
            } finally {
                long elapsed = (System.currentTimeMillis() - s);
                if (elapsed > LARGE_ELAPSED_TIME) {
                    log.warn("HttpUtil request uri:{} and elapsed time is:{}", request.getURI(), elapsed);
                } else {
                    log.debug("HttpUtil request uri:{}, and elapsed time is:{}", request.getURI(), elapsed);
                }
            }
            StatusLine statusLine = response.getStatusLine();
            int status = statusLine.getStatusCode();
            if (status != HTTP_SUCC) {
                throw new IOException("code:[" + status + "],reason:[" + statusLine.getReasonPhrase() + "]");
            }
            try {
                //EntityUtils.toString方法会执行instream.close()操作
                return EntityUtils.toString(response.getEntity());
            } catch (IOException e) {
                throw new IOException("response write data error!", e);
            }
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    throw new IOException("response close error!", e);
                }
            }
        }
    }

    static class ApacheHttpClientDelegate {
        private final int keepAlive;
        private final int maxTotal;
        private final int perRoute;
        private final int connectionTimeout;
        private final int socketTimeout;
        private final int retryCount;
        private static final int DEFAULT_VALIDATE_CONNECTION_TIME = 5000;
        private static final int CONNECTION_FULL_TIMEOUT = 9000;
        private static final int DEFAULT_KEEP_ALIVE = 90000;
        private static final int DEFAULT_MAX_ROUTE = 20;
        private static final int DEFAULT_PER_ROUTE = 10;
        private static final int DEFAULT_CONNECTION_TIMEOUT = 3000;
        private static final int DEFAULT_SOCKET_TIMEOUT = 5000;
        private static final int DEFAULT_IDLE_TIMEOUT = 300;
        private static final int DEFAULT_RETRY_COUNT = 1;

        int getInt(Integer value, int def) {
            try {
                return value.intValue();
            } catch (Exception e) {
                return def;
            }
        }

        ApacheHttpClientDelegate(HttpClientProperties properties) {
            this.keepAlive = getInt(properties.getKeepAlive(), DEFAULT_KEEP_ALIVE);
            this.maxTotal = getInt(properties.getMaxRoute(), DEFAULT_MAX_ROUTE);
            this.perRoute = getInt(properties.getPerRoute(), DEFAULT_PER_ROUTE);
            this.connectionTimeout = getInt(properties.getConnectionTimeout(), DEFAULT_CONNECTION_TIMEOUT);
            this.socketTimeout = getInt(properties.getSocketTimeout(), DEFAULT_SOCKET_TIMEOUT);
            this.retryCount = getInt(properties.getRetryCount(), DEFAULT_RETRY_COUNT);
            log.info("HttpUtil initial apache http client delegate, keepAlive:{}, maxTotal:{}, perRoute:{}, connectionTimeout:{}, " +
                    "socketTimeout:{}", keepAlive, maxTotal, perRoute, connectionTimeout, socketTimeout);
        }

        CloseableHttpClient create() {
            ConnectionKeepAliveStrategy myStrategy = (response, context) -> {
                HeaderElementIterator it = new BasicHeaderElementIterator(
                        response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    // 如果客户端返回值中带有timeout字段，则以这个字段作为keep-alive时长
                    if (value != null && "timeout".equalsIgnoreCase(param)) {
                        return Long.parseLong(value) * 1000;
                    }
                }
                //如果没有约定，则默认定义时长为90s，90s后会被认为过期的连接，然后由定时任务清理
                return keepAlive;
            };

            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
            // 设置最大的连接数，即整个池都不能超过的连接数
            connectionManager.setMaxTotal(maxTotal);
            // 设置每一个路由的连接数(dest ip + dest port)，路由指的是目的路由，而非原路由
            connectionManager.setDefaultMaxPerRoute(perRoute);
            // 间隔多少时间检测下链接是否还存活
            connectionManager.setValidateAfterInactivity(DEFAULT_VALIDATE_CONNECTION_TIME);

            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectTimeout(connectionTimeout)
                    .setSocketTimeout(socketTimeout)
                    // 连接不够用时等待超时时间，一定要设置，如果不设置的话，如果连接池连接不够用，就会线程阻塞
                    .setConnectionRequestTimeout(CONNECTION_FULL_TIMEOUT)
                    .build();

            ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
                    1, new CustomizableThreadFactory("httpclient-idle-evict-worker"));
            executor.scheduleAtFixedRate(new IdleConnectionEvictionTask(connectionManager), 60, 60, TimeUnit.SECONDS);
            // 请求重试处理
            HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
                @Override
                public boolean retryRequest(IOException exception,
                                            int executionCount, HttpContext context) {
                    if (executionCount >= retryCount) {
                        return false;
                    }
                    if (exception instanceof NoHttpResponseException) {// 如果服务器丢掉了连接，那么就重试
                        return true;
                    }
                    if (exception instanceof SSLHandshakeException) {// 不要重试SSL握手异常
                        return false;
                    }
                    if (exception instanceof InterruptedIOException) {// 超时
                        return false;
                    }
                    if (exception instanceof UnknownHostException) {// 目标服务器不可达
                        return false;
                    }
                    if (exception instanceof ConnectTimeoutException) {// 连接被拒绝
                        return false;
                    }
                    if (exception instanceof SSLException) {// SSL握手异常
                        return false;
                    }
                    HttpClientContext clientContext = HttpClientContext
                            .adapt(context);
                    HttpRequest request = clientContext.getRequest();
                    // 如果请求是幂等的，就再次尝试
                    if (!(request instanceof HttpEntityEnclosingRequest)) {
                        return true;
                    }
                    return false;
                }
            };
            return HttpClients.custom()
                    .setConnectionManager(connectionManager)
                    .setKeepAliveStrategy(myStrategy)
                    .setDefaultRequestConfig(requestConfig)
                    .setRetryHandler(httpRequestRetryHandler)
                    .build();
        }

        private class IdleConnectionEvictionTask implements Runnable {

            private PoolingHttpClientConnectionManager manager;

            IdleConnectionEvictionTask(PoolingHttpClientConnectionManager manager) {
                this.manager = manager;
            }

            @Override
            public void run() {
                manager.closeExpiredConnections();
                manager.closeIdleConnections(DEFAULT_IDLE_TIMEOUT, TimeUnit.SECONDS);
                PoolStats st = manager.getTotalStats();
                log.info("HttpUtil current pooling connection status,available:{}, max:{}, lease:{}, pending:{}",
                        st.getAvailable(), st.getMax(), st.getLeased(), st.getPending());
            }
        }
    }
}